/*
Copyright 2011 Google Inc. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package com.google.rsm.core;

import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.Map;

/**
 * A runtime "compiler" for creating a generic State Machine.
 * TODO: Add more documetation.
 * TODO: Add meta transition methods (forAllEvents etc).
 *
 * @author arunabhaghosh@google.com (Arunabha Ghosh)
 */
public class StateCreator {
  private static final Logger logger = Logger.getLogger(StateCreator.class.getName());

  private static class HookEntry {
    public final String state;
    public final EventType eventType;
    public final EventCallback callback;
    public HookEntry(
        String state, EventType eventType, EventCallback callback) {
      this.state = state;
      this.eventType = eventType;
      this.callback = callback;
    }
  }

  private static class StateHookEntry {
    public final String state;
    public final StateCallback callback;
    public StateHookEntry(String state, StateCallback callback) {
      this.state = state;
      this.callback = callback;
    }
  }

  public static class TransitionEntry {
    public final String state;
    public final EventType eventType;
    public final String nextState;
    public TransitionEntry(
        String state, EventType event, String nextState) {
      this.state = state;
      this.eventType = event;
      this.nextState = nextState;
    }
  }

  private int stateCount;
  private int eventCount;
  private final HashMap<String, Integer> states;
  private final HashMap<EventType, Integer> events;
  private final LinkedList<HookEntry> hooks;
  private final LinkedList<StateHookEntry> stateHooks;
  private final LinkedList<TransitionEntry> transitions;
  private final String name;
  private int startState;

  /**
   * The StateMachines corresponding to individual states (if
   * any). This allows the StateCreator and GenericStateMachine to
   * support true recursive state machines. We can have an individual
   * state in the StateMachine be a StateMachine itself. This map
   * stores the StateMachine corresponding to each State (null if the
   * state is a simple state and has no associated State Machine).
   */
  private final HashMap<String, State> stateMachines;

  /**
   * Indicates whether this object has been compiled.
   */
  private boolean compiled;

  /**
   * Indicates whether strict checking is done. If strictCheck is
   * enabled(true) then any attempt to refer to a state or event not
   * previously declared (via addState and addEvent) will result in a
   * runtime exception. If strictCheck is disabled then a reference to
   * a an undeclared state or event causes it to be implicitly
   * declared.  NOTE: only simple states are added implicitly. There
   * is no way of adding recursive state implicitly.
   */
  private boolean strictCheck;

  public StateCreator(String name) {
    Preconditions.checkNotNull(name);
    this.name = name;
    stateCount = 0;
    eventCount = 0;
    states = Maps.newHashMap();
    stateMachines = Maps.newHashMap();
    events = Maps.newHashMap();
    hooks = new LinkedList<HookEntry>();
    stateHooks = new LinkedList<StateHookEntry>();
    transitions = new LinkedList<TransitionEntry>();
    compiled = false;
    startState = 0; // By default the first state.
    strictCheck = true; // By default strictCheck is enabled.
  }

  /**
   * Checks whether this state machine has already been compiled and
   * throws an {@code IllegalStateException} if it has.
   */
  private void checkCompiled() {
    if (compiled) {
      throw new IllegalStateException(
          String.format("StateMachine %s has already been compiled", this));
    }
  }

  /** Disables strict checking of events and state. */
  public void disableStrictCheck() {
    strictCheck = false;
  }

  /** Enables strict checking of events and state. */
  public void enableStrictCheck() {
    strictCheck = true;
  }

 /**
   * Returns the index of the given state, throws an {@code
   * IllegalArgumentException} if the given state does not exist.
   */
  private int getStateIndex(String state) {
    if (!states.containsKey(state)) {
      throw new IllegalArgumentException(
          String.format("Unknown state %s in StateMachine %s", state, this));
    } else {
      return states.get(state);
    }
  }

  /**
   * Returns the index of the given event, throws an {@code
   * IllegalArgumentException} if the given event does not exist.
   */
  private int getEventIndex(EventType event) {
    if (!events.containsKey(event)) {
      throw new IllegalArgumentException(
          String.format("Unknown event %s in StateMachine %s", event, this));
    } else {
      return events.get(event);
    }
  }

  /**
   * Returns the StateMachine associated with a given recursive
   * State. Throws an IllegalArgumentException if the given State is
   * not known. Note that null is a valid return value.
   */
  State getStateMachine(String stateName) {
    Preconditions.checkNotNull(stateName);
    Preconditions.checkArgument(stateMachines.containsKey(stateName));
    return stateMachines.get(stateName);
  }

  /**
   * Add a simple state to the State Machine. A simple state is a
   * state which is not a state mahcine itself. Will throw a {@code
   * IllegaArgumentException} if the state already exists.
   */
  public void addState(String state) {
    Preconditions.checkNotNull(state);
    checkCompiled();
    if (states.containsKey(state)) {
      throw new IllegalArgumentException(
          String.format("addState: State %s already exists in State Machine %s",
                        state,
                        this));
    }
    states.put(state, stateCount);
    stateMachines.put(state, null);
    ++stateCount;
  }

  /**
   * Add a complex(more technically, recursive) state to the State
   * Machine. A recursive state is a state which is itself a state
   * machine. Will throw a {@code IllegaArgumentException} if the
   * state already exists.
   */
  public void addState(String state, State stateMachine) {
    Preconditions.checkNotNull(state);
    Preconditions.checkNotNull(stateMachine);
    checkCompiled();
    if (states.containsKey(state)) {
      throw new IllegalArgumentException(
          String.format("addState: State %s already exists in State Machine %s",
                        state,
                        this));
    }

    states.put(state, stateCount);
    stateMachines.put(state, stateMachine);
    ++stateCount;
  }

  /**
   * Add an event to the State Machine. Will throw a {@code
   * IllegaArgumentException} if the event already exists.
   */
  public void addEvent(EventType event) {
    Preconditions.checkNotNull(event);
    checkCompiled();
    if (events.containsKey(event)) {
      throw new IllegalArgumentException(
          String.format("addEvent: Event %s already exists in State Machine %s",
                        event,
                        this));
    }
    events.put(event, eventCount);
    ++eventCount;
  }

  /**
   * Adds an {@code EventCallback} to be called when the specified
   * combination of current state and event occur. Throws an {@code
   * IllegalArgumentException} if the specified state or event does
   * not exist.
   *
   * @param state The value the currentState has to be for this hook
   * to be called.
   * @param eventType The event which must be received.
   * @param callback The callback which is to be called.
   */
  public void addHook(
      String state, EventType eventType, EventCallback callback) {
    Preconditions.checkNotNull(state);
    Preconditions.checkNotNull(eventType);
    Preconditions.checkNotNull(callback);
    checkCompiled();

    // If strictCheck is disabled, add the state and event if they
    // dont exist.
    if (!strictCheck) {
      addIfNotPresent(state);
      addIfNotPresent(eventType);
    }

    // Ensure that the state and event exist.
    if (!states.containsKey(state)) {
      throw new IllegalArgumentException(
          String.format("addHook: State %s does not exist in State Machine %s",
                        state,
                        this));
    }
    if (!events.containsKey(eventType)) {
      throw new IllegalArgumentException(
          String.format("addHook: Event %s does not exist in State Machine %s",
                        eventType,
                        this));
    }
    hooks.add(new HookEntry(state, eventType, callback));
  }

  /**
   * Adds a {@code StateCallback} to be called when the specified
   * state is transitioned into. Throws an {@code
   * IllegalArgumentException} if the specified state or event does
   * not exist.
   *
   * @param state The state which has to be transitioned into.
   * @param callback The callback which is to be called.
   */
  public void addStateHook(String state, StateCallback callback) {
    Preconditions.checkNotNull(state);
    Preconditions.checkNotNull(callback);
    checkCompiled();

    // If strictCheck is disabled, add the state if it doesn't exist.
    if (!strictCheck) {
      addIfNotPresent(state);
    }

    // Ensure that the state exists.
    if (!states.containsKey(state)) {
      throw new IllegalArgumentException(
          String.format("addStateHook: State %s does not exist in State Machine"
                        + " %s",
                        state,
                        this));
    }
    stateHooks.add(new StateHookEntry(state, callback));
  }

  /**
   * Define the transition to make on the specified combination of
   * current state and event. Throws an {@code
   * IllegalArgumentException} if the specified states or events do
   * not exist.
   * @param fromState The value the currentState has to be for this transition.
   * @param eventType The event which must be received.
   * @param toState The state to which to transition to.
   */
  public void addTransition(
      String fromState, EventType eventType, String toState) {
    Preconditions.checkNotNull(fromState);
    Preconditions.checkNotNull(eventType);
    Preconditions.checkNotNull(toState);
    checkCompiled();

    // If strictCheck is disabled, add the state and event if they
    // dont exist.
    if (!strictCheck) {
      addIfNotPresent(toState);
      addIfNotPresent(fromState);
      addIfNotPresent(eventType);
    }

    // See if the states and event exist.
    if (!states.containsKey(fromState)) {
      throw new IllegalArgumentException(
          String.format("addTransition: State %s does not exist in"
                        + " State Machine %s",
                        fromState,
                        this));
    }
    if (!states.containsKey(toState)) {
      throw new IllegalArgumentException(
          String.format("addTransition: State %s does not exist in"
                        + " State Machine %s",
                        toState,
                        this));
    }
    if (!events.containsKey(eventType)) {
      throw new IllegalArgumentException(
          String.format("addTransition: Event %s does not exist in"
                        + " State Machine %s",
                        eventType,
                        this));
    }
    transitions.add(new TransitionEntry(fromState, eventType, toState));
  }

  /**
   * Sets the starting state of the generated machine. If no starting
   * state is specified then the first state is taken to be the start
   * state. Throws an {@code InvalidArgumentException} if an invalid
   * state is given..
   */
  public void setStartState(String startState) {
    Preconditions.checkNotNull(startState);
    checkCompiled();
    // If the given state is not known then getStateIndex will throw
    // an exception.
    int startIndex = getStateIndex(startState);
    this.startState = startIndex;
  }

  /**
   * Makes the specified state ignore the given Events. This is done
   * by having the state transition to itself upon receiving the
   * specified Events. Note that any state hooks associated with the
   * state will still be called when the self transition happens due
   * to an ignored event.
   */
  public void ignoreEvents(String stateName,
                           EventType... ignoredEventTypes) {
    Preconditions.checkNotNull(stateName);
    Preconditions.checkNotNull(ignoredEventTypes);
    for (EventType e : ignoredEventTypes) {
      addTransition(stateName, e, stateName);
    }
  }

  /**
   * Returns a compiled {@code GenericStateMachine} which implements
   * the StateMachine specified in this object. Once compile has been
   * called on a StateCreator no further changes can be made to the
   * object and future calls to compile will always return the same
   * StateMachine. Will throw an IllegalStateException if it
   * encounters an error condition such as multiply defined
   * transitions.
   */
  public GenericStateMachine compile() {
    Preconditions.checkArgument((stateCount > 0),
                                "Cannot compile a StateMachine with no states");

    Preconditions.checkArgument((eventCount > 0),
                                "Cannot compile a StateMachine with no events");

    // Create the transition table and the hook table.
    int[][] transitionTable = new int[stateCount][eventCount];

    // We have to suppress warnings here as the compiler wont allow an
    // expression involving generics in the new operator (really!!).
    @SuppressWarnings("unchecked")
    List<EventCallback>[][] hookTable = new List[stateCount][eventCount];

    // Initialize tables;
    for (int i = 0; i < stateCount; i++) {
      for (int j = 0; j < eventCount; j++) {
        transitionTable[i][j] = -1;
        hookTable[i][j] = null;
      }
    }

    // Build the transitionTable
    for (TransitionEntry e : transitions) {
      int fromIndex = getStateIndex(e.state);
      int toIndex = getStateIndex(e.nextState);
      int eIndex = getEventIndex(e.eventType);
      // See if this transition has already been defined.
      if (transitionTable[fromIndex][eIndex] != -1) {
        throw new IllegalStateException(
            String.format("Multiply defined transition for state %s and event"
                          + " %s in StateMachine %s",
                          e.state,
                          e.eventType,
                          this));
      } else {
        // Define the transition.
        transitionTable[fromIndex][eIndex] = toIndex;
      }
    }

    // Build the hook table.
    for (HookEntry e : hooks) {
      int stateIndex = getStateIndex(e.state);
      int eventIndex = getEventIndex(e.eventType);
      List<EventCallback> callbackList;
      if (hookTable[stateIndex][eventIndex] == null) {
        callbackList = new LinkedList<EventCallback>();
        hookTable[stateIndex][eventIndex] = callbackList;
      } else {
        // We already have a callback list.
        callbackList = hookTable[stateIndex][eventIndex];
      }
      callbackList.add(e.callback);
    }
    String[] stateNames = new String[stateCount];
    State[] stateMachinesArray = new State[stateCount];

    /*
     * Build the array of state names and the array listing
     * StateMachines for each state.
     */
    for (String stateName : states.keySet()) {
      int nameIndex = getStateIndex(stateName);
      stateNames[nameIndex] = stateName;
      stateMachinesArray[nameIndex] = getStateMachine(stateName);
    }

    // Create the StateHooks array, and initialize it.
    @SuppressWarnings("unchecked")
    List<StateCallback>[] stateHookArray = new List[stateCount];
    for (int i = 0; i < stateCount; i++) {
      stateHookArray[i] = null;
    }
    for (StateHookEntry e : stateHooks) {
      int stateIndex = getStateIndex(e.state);
      List<StateCallback> callbackList;
      if (stateHookArray[stateIndex] == null) {
        callbackList = new LinkedList<StateCallback>();
        stateHookArray[stateIndex] = callbackList;
      } else {
        callbackList = stateHookArray[stateIndex];
      }
      callbackList.add(e.callback);
    }

    // Create the {@code GenericStateMachine}.
    GenericStateMachine compiledStateMachine =
        new GenericStateMachine(states,
                                events,
                                transitionTable,
                                hookTable,
                                stateHookArray,
                                stateNames,
                                stateMachinesArray,
                                this.toString(),
                                startState);
    compiled = true;
    return compiledStateMachine;
  }

  /**
   * Checks to see if the given state is already known, if not it is
   * added to the list of known states.
   */
  private void addIfNotPresent(String state) {
    if (!states.containsKey(state)) {
      addState(state);
      logger.finest(String.format("Added implicit state %s in %s", state, this));
    }
  }

  /**
   * Checks to see if the given event is already known, if not it is
   * added to the list of known events.
   */
  private void addIfNotPresent(EventType eventType) {
    if (!events.containsKey(eventType)) {
      addEvent(eventType);
      logger.finest(String.format("Added implicit event %s in%s", eventType, this));
    }
  }

  @Override
  public String toString() {
    return "GenericStateMachine:" + name;
  }

  /**
   * Get a copy of states, where each state is a pair of the state's name and
   * an indicator of whether the state is a state machine.
   * Note that this format <String, Boolean> is different from the format of
   * the original states, which is <String, Integer>.
   *
   * This method is not thread-safe.
   */
  public Map<String, Boolean> getCopyOfStates() {
    Map<String, Boolean> copiedStates = new HashMap<String, Boolean>();

    for (String s : stateMachines.keySet()) {
      if (stateMachines.get(s) != null) {
        copiedStates.put(s, Boolean.TRUE);
      } else {
        copiedStates.put(s, Boolean.FALSE);
      }
    }

    return copiedStates;
  }

  /**
   * Get a copy of transitions.
   *
   * This method is not thread-safe.
   */
  public LinkedList<TransitionEntry> getCopyOfTransitions() {
    LinkedList<TransitionEntry> copiedTransitions
        = new LinkedList<TransitionEntry>();
    TransitionEntry tmpTransition;
    for (TransitionEntry e : transitions) {
      tmpTransition = new TransitionEntry(e.state, e.eventType, e.nextState);
      copiedTransitions.add(tmpTransition);
    }
    return copiedTransitions;
  }
}
